﻿using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;

namespace openSourceC.DaylifeClient
{
	/// <summary>
	///		Summary description for Request.
	/// </summary>
	public class Request
	{
		//private const int _commercialLicenseCoreParamterLimit = 100;
		//private const int _freeLicenseCoreParamterLimit = 10;

		private string _accessKey;

		private SortedSet<string> _coreParameterValues;
		private List<ParameterEntry> _parameters;

		private ParameterEntry _accessKeyParameterEntry;
		private ParameterEntry _signatureParameterEntry;

		private class ParameterEntry
		{
			public string Key;
			public string Value;


			public ParameterEntry(string key, string value)
			{
				Key = key;
				Value = value;
			}
		}


		#region Constructors

		/// <summary>
		///		Initializes a new instance of <see cref="Request"/> with the specified
		///		method name, access key, and shared secret.
		/// </summary>
		/// <param name="methodName">The name of the Daylife API method to call.</param>
		/// <param name="accessKey">The access key.</param>
		/// <param name="sharedSecret">The shared secret.</param>
		internal Request(string methodName, string accessKey, string sharedSecret)
		{
			MethodName = methodName;
			_accessKey = accessKey;
			SharedSecret = sharedSecret;

			_coreParameterValues = new SortedSet<string>(StringComparer.Ordinal);

			_parameters = new List<ParameterEntry>();
			_parameters.Add(_accessKeyParameterEntry = new ParameterEntry("accesskey", _accessKey));
			_parameters.Add(_signatureParameterEntry = new ParameterEntry("signature", null));
		}

		#endregion

		#region Public Properties

		/// <summary>Gets or sets the access key.</summary>
		public string AccessKey
		{
			get { return _accessKey; }
			set { _accessKey = value; _accessKeyParameterEntry.Value = _accessKey; }
		}

		/// <summary>Gets or sets the name of the Daylife API method to call.</summary>
		public string MethodName { get; set; }

		/// <summary>Gets or sets the shared secret.</summary>
		public string SharedSecret { get; set; }

		#endregion

		#region AppendParameter Methods

		/// <summary>
		///		Appends a 32-bit signed integer number to the parameters collection.
		/// </summary>
		/// <param name="key">The query-string key to append.</param>
		/// <param name="isCoreParameter">Indicates that this is a core parameter.</param>
		/// <param name="value">The query-string value to append.</param>
		/// <param name="defaultValue">The query-string value to append if value is null or empty. (optional)</param>
		internal void AppendParameter<TEnum>(string key, bool isCoreParameter, TEnum? value, TEnum? defaultValue = null)
			where TEnum : struct
		{
			if (value.HasValue && !(value is Enum))
			{
				throw new SystemException("TEnum is not an enumerator.");
			}

			if ((value = (value.HasValue ? value : defaultValue)).HasValue)
			{
				AppendParameter(key, isCoreParameter, value.Value.ToString().ToLower());
			}
		}

		/// <summary>
		///		Appends a Boolean value to the parameters collection.
		/// </summary>
		/// <param name="key">The query-string key to append.</param>
		/// <param name="isCoreParameter">Indicates that this is a core parameter.</param>
		/// <param name="value">The query-string value to append.</param>
		/// <param name="defaultValue">The query-string value to append if value is null or empty. (optional)</param>
		internal void AppendParameter(string key, bool isCoreParameter, bool? value, bool? defaultValue = null)
		{
			if ((value = (value.HasValue ? value : defaultValue)).HasValue)
			{
				AppendParameter(key, isCoreParameter, (value.Value ? "1" : "0"));
			}
		}

		/// <summary>
		///		Appends an 8-bit unsigned integer to the parameters collection.
		/// </summary>
		/// <param name="key">The query-string key to append.</param>
		/// <param name="isCoreParameter">Indicates that this is a core parameter.</param>
		/// <param name="value">The query-string value to append.</param>
		/// <param name="defaultValue">The query-string value to append if value is null or empty. (optional)</param>
		internal void AppendParameter(string key, bool isCoreParameter, byte? value, byte? defaultValue = null)
		{
			if ((value = (value.HasValue ? value : defaultValue)).HasValue)
			{
				AppendParameter(key, isCoreParameter, value.Value.ToString());
			}
		}

		/// <summary>
		///		Appends a Unicode character to the parameters collection.
		/// </summary>
		/// <param name="key">The query-string key to append.</param>
		/// <param name="value">The query-string value to append.</param>
		/// <param name="defaultValue">The query-string value to append if value is null or empty. (optional)</param>
		/// <param name="isCoreParameter">Indicates that this is a core parameter.</param>
		internal void AppendParameter(string key, bool isCoreParameter, char? value, char? defaultValue = null)
		{
			if ((value = (value.HasValue ? value : defaultValue)).HasValue)
			{
				AppendParameter(key, isCoreParameter, value.Value.ToString());
			}
		}

		/// <summary>
		///		Appends a Unicode character array to the parameters collection.
		/// </summary>
		/// <param name="key">The query-string key to append.</param>
		/// <param name="value">The query-string value to append.</param>
		/// <param name="defaultValue">The query-string value to append if value is null or empty. (optional)</param>
		/// <param name="isCoreParameter">Indicates that this is a core parameter.</param>
		internal void AppendParameter(string key, bool isCoreParameter, char[] value, char[] defaultValue = null)
		{
			if ((value = (value ?? defaultValue)) != null)
			{
				AppendParameter(key, isCoreParameter, new string(value));
			}
		}

		/// <summary>
		///		Appends a decimal number to the parameters collection.
		/// </summary>
		/// <param name="key">The query-string key to append.</param>
		/// <param name="isCoreParameter">Indicates that this is a core parameter.</param>
		/// <param name="value">The query-string value to append.</param>
		/// <param name="defaultValue">The query-string value to append if value is null or empty. (optional)</param>
		internal void AppendParameter(string key, bool isCoreParameter, decimal? value, decimal? defaultValue = null)
		{
			if ((value = (value.HasValue ? value : defaultValue)).HasValue)
			{
				AppendParameter(key, isCoreParameter, value.Value.ToString());
			}
		}

		/// <summary>
		///		Appends a double-precision floating-point number to the parameters collection.
		/// </summary>
		/// <param name="key">The query-string key to append.</param>
		/// <param name="isCoreParameter">Indicates that this is a core parameter.</param>
		/// <param name="value">The query-string value to append.</param>
		/// <param name="defaultValue">The query-string value to append if value is null or empty. (optional)</param>
		internal void AppendParameter(string key, bool isCoreParameter, double? value, double? defaultValue = null)
		{
			if ((value = (value.HasValue ? value : defaultValue)).HasValue)
			{
				AppendParameter(key, isCoreParameter, value.Value.ToString());
			}
		}

		/// <summary>
		///		Appends a single-precision floating-point number to the parameters collection.
		/// </summary>
		/// <param name="key">The query-string key to append.</param>
		/// <param name="isCoreParameter">Indicates that this is a core parameter.</param>
		/// <param name="value">The query-string value to append.</param>
		/// <param name="defaultValue">The query-string value to append if value is null or empty. (optional)</param>
		internal void AppendParameter(string key, bool isCoreParameter, float? value, float? defaultValue = null)
		{
			if ((value = (value.HasValue ? value : defaultValue)).HasValue)
			{
				AppendParameter(key, isCoreParameter, value.Value.ToString());
			}
		}

		/// <summary>
		///		Appends an <see cref="IEnumerable&lt;T&gt;"/> collection of type
		///		<see cref="string"/> to the parameters collection.
		/// </summary>
		/// <param name="key">The query-string key to append.</param>
		/// <param name="isCoreParameter">Indicates that this is a core parameter.</param>
		/// <param name="value">The query-string value to append.</param>
		/// <param name="defaultValue">The query-string value to append if value is null or empty. (optional)</param>
		internal void AppendParameter(string key, bool isCoreParameter, IEnumerable<string> value, IEnumerable<string> defaultValue = null)
		{
			if ((value = (value ?? defaultValue)) != null)
			{
				foreach (string item in value)
				{
					AppendParameter(key, isCoreParameter, item);
				}
			}
		}

		/// <summary>
		///		Appends a 32-bit signed integer number to the parameters collection.
		/// </summary>
		/// <param name="key">The query-string key to append.</param>
		/// <param name="isCoreParameter">Indicates that this is a core parameter.</param>
		/// <param name="value">The query-string value to append.</param>
		/// <param name="defaultValue">The query-string value to append if value is null or empty. (optional)</param>
		internal void AppendParameter(string key, bool isCoreParameter, int? value, int? defaultValue = null)
		{
			if ((value = (value.HasValue ? value : defaultValue)).HasValue)
			{
				AppendParameter(key, isCoreParameter, value.Value.ToString());
			}
		}

		/// <summary>
		///		Appends a 64-bit signed integer number to the parameters collection.
		/// </summary>
		/// <param name="key">The query-string key to append.</param>
		/// <param name="isCoreParameter">Indicates that this is a core parameter.</param>
		/// <param name="value">The query-string value to append.</param>
		/// <param name="defaultValue">The query-string value to append if value is null or empty. (optional)</param>
		internal void AppendParameter(string key, bool isCoreParameter, long? value, long? defaultValue = null)
		{
			if ((value = (value.HasValue ? value : defaultValue)).HasValue)
			{
				AppendParameter(key, isCoreParameter, value.Value.ToString());
			}
		}

		/// <summary>
		///		Appends an 8-bit signed integer to the parameters collection.
		/// </summary>
		/// <param name="key">The query-string key to append.</param>
		/// <param name="isCoreParameter">Indicates that this is a core parameter.</param>
		/// <param name="value">The query-string value to append.</param>
		/// <param name="defaultValue">The query-string value to append if value is null or empty. (optional)</param>
		internal void AppendParameter(string key, bool isCoreParameter, sbyte? value, sbyte? defaultValue = null)
		{
			if ((value = (value.HasValue ? value : defaultValue)).HasValue)
			{
				AppendParameter(key, isCoreParameter, value.Value.ToString());
			}
		}

		/// <summary>
		///		Appends a 16-bit signed integer number to the parameters collection.
		/// </summary>
		/// <param name="key">The query-string key to append.</param>
		/// <param name="isCoreParameter">Indicates that this is a core parameter.</param>
		/// <param name="value">The query-string value to append.</param>
		/// <param name="defaultValue">The query-string value to append if value is null or empty. (optional)</param>
		internal void AppendParameter(string key, bool isCoreParameter, short? value, short? defaultValue = null)
		{
			if ((value = (value.HasValue ? value : defaultValue)).HasValue)
			{
				AppendParameter(key, isCoreParameter, value.Value.ToString());
			}
		}

		/// <summary>
		///		Appends a <see cref="DateTime"/> value to the parameters collection.
		/// </summary>
		/// <param name="key">The query-string key to append.</param>
		/// <param name="isCoreParameter">Indicates that this is a core parameter.</param>
		/// <param name="dateTimeFormat">The DateTime format to use.</param>
		/// <param name="value">The query-string value to append.</param>
		/// <param name="defaultValue">The query-string value to append if value is null or empty. (optional)</param>
		internal void AppendParameter(string key, bool isCoreParameter, string dateTimeFormat, DateTime? value, DateTime? defaultValue = null)
		{
			if ((value = (value.HasValue ? value : defaultValue)).HasValue)
			{
				AppendParameter(key, isCoreParameter, value.Value.ToString(dateTimeFormat));
			}
		}

		/// <summary>
		///		Appends a <see cref="DateTimeOffset"/> value to the parameters collection after converting it to a <see cref="DateTime"/> value.
		/// </summary>
		/// <param name="key">The query-string key to append.</param>
		/// <param name="isCoreParameter">Indicates that this is a core parameter.</param>
		/// <param name="dateTimeFormat">The DateTime format to use.</param>
		/// <param name="value">The query-string value to append.</param>
		/// <param name="kind">The <see cref="E:DateTimeKind"/> to use for the conversion.</param>
		/// <param name="defaultValue">The query-string value to append if value is null or empty. (optional)</param>
		internal void AppendParameter(string key, bool isCoreParameter, string dateTimeFormat, DateTimeOffset? value, DateTimeKind kind, DateTimeOffset? defaultValue = null)
		{
			if ((value = (value.HasValue ? value : defaultValue)).HasValue)
			{
				switch (kind)
				{
					case DateTimeKind.Local:
					{
						AppendParameter(key, isCoreParameter, value.Value.LocalDateTime.ToString(dateTimeFormat));
						break;
					}

					case DateTimeKind.Utc:
					{
						AppendParameter(key, isCoreParameter, value.Value.UtcDateTime.ToString(dateTimeFormat));
						break;
					}

					case DateTimeKind.Unspecified:
					default:
					{
						AppendParameter(key, isCoreParameter, value.Value.DateTime.ToString(dateTimeFormat));
						break;
					}
				}
			}
		}

		/// <summary>
		///		Appends a <see cref="DateTimeOffset"/> value to the parameters collection.
		/// </summary>
		/// <param name="key">The query-string key to append.</param>
		/// <param name="isCoreParameter">Indicates that this is a core parameter.</param>
		/// <param name="dateTimeFormat">The DateTime format to use.</param>
		/// <param name="value">The query-string value to append.</param>
		/// <param name="defaultValue">The query-string value to append if value is null or empty. (optional)</param>
		internal void AppendParameter(string key, bool isCoreParameter, string dateTimeFormat, DateTimeOffset? value, DateTimeOffset? defaultValue = null)
		{
			if ((value = (value.HasValue ? value : defaultValue)).HasValue)
			{
				AppendParameter(key, isCoreParameter, value.Value.ToString(dateTimeFormat));
			}
		}

		/// <summary>
		///		Appends a <see cref="string"/> to the parameters collection.
		/// </summary>
		/// <param name="key">The query-string key to append.</param>
		/// <param name="isCoreParameter">Indicates that this is a core parameter.</param>
		/// <param name="value">The query-string value to append.</param>
		/// <param name="defaultValue">The query-string value to append if value is null or empty. (optional)</param>
		internal void AppendParameter(string key, bool isCoreParameter, string value, string defaultValue = null)
		{
			if (!string.IsNullOrEmpty(value = (string.IsNullOrEmpty(value) ? defaultValue : value)))
			{
				if (isCoreParameter)
				{
					_coreParameterValues.Add(value);
				}

				_parameters.Add(new ParameterEntry(key, value));
			}
		}

		/// <summary>
		///		Appends a <see cref="string"/> array to the parameters collection.
		/// </summary>
		/// <param name="key">The query-string key to append.</param>
		/// <param name="isCoreParameter">Indicates that this is a core parameter.</param>
		/// <param name="value">The query-string value to append.</param>
		/// <param name="defaultValue">The query-string value to append if value is null or empty. (optional)</param>
		internal void AppendParameter(string key, bool isCoreParameter, string[] value, string[] defaultValue = null)
		{
			if ((value = (value ?? defaultValue)) != null)
			{
				foreach (string item in value)
				{
					AppendParameter(key, isCoreParameter, item);
				}
			}
		}

		/// <summary>
		///		Appends a 32-bit unsigned integer number to the parameters collection.
		/// </summary>
		/// <param name="key">The query-string key to append.</param>
		/// <param name="isCoreParameter">Indicates that this is a core parameter.</param>
		/// <param name="value">The query-string value to append.</param>
		/// <param name="defaultValue">The query-string value to append if value is null or empty. (optional)</param>
		internal void AppendParameter(string key, bool isCoreParameter, uint? value, uint? defaultValue = null)
		{
			if ((value = (value.HasValue ? value : defaultValue)).HasValue)
			{
				AppendParameter(key, isCoreParameter, value.Value.ToString());
			}
		}

		/// <summary>
		///		Appends a 64-bit unsigned integer number to the parameters collection.
		/// </summary>
		/// <param name="key">The query-string key to append.</param>
		/// <param name="isCoreParameter">Indicates that this is a core parameter.</param>
		/// <param name="value">The query-string value to append.</param>
		/// <param name="defaultValue">The query-string value to append if value is null or empty. (optional)</param>
		internal void AppendParameter(string key, bool isCoreParameter, ulong? value, ulong? defaultValue = null)
		{
			if ((value = (value.HasValue ? value : defaultValue)).HasValue)
			{
				AppendParameter(key, isCoreParameter, value.Value.ToString());
			}
		}

		/// <summary>
		///		Appends a 16-bit unsigned integer number to the parameters collection.
		/// </summary>
		/// <param name="key">The query-string key to append.</param>
		/// <param name="isCoreParameter">Indicates that this is a core parameter.</param>
		/// <param name="value">The query-string value to append.</param>
		/// <param name="defaultValue">The query-string value to append if value is null or empty. (optional)</param>
		internal void AppendParameter(string key, bool isCoreParameter, ushort? value, ushort? defaultValue = null)
		{
			if ((value = (value.HasValue ? value : defaultValue)).HasValue)
			{
				AppendParameter(key, isCoreParameter, value.Value.ToString());
			}
		}

		#endregion

		#region Public Methods

		/// <summary>
		///		Gets a string that is encoded and usable in the query-string portion of a url.
		/// </summary>
		/// <returns></returns>
		public string GetQueryString()
		{
			if (_coreParameterValues.Count == 0)
			{
				throw new InvalidOperationException("No core parameters found.");
			}

			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.Append(_accessKey).Append(SharedSecret);

			foreach (string coreValue in _coreParameterValues)
			{
				stringBuilder.Append(coreValue);
			}

			MD5CryptoServiceProvider cryptoProvider = new MD5CryptoServiceProvider();
			byte[] buffer = cryptoProvider.ComputeHash(UnicodeEncoding.UTF8.GetBytes(stringBuilder.ToString()));

			stringBuilder = new StringBuilder(32);

			foreach (byte b in buffer)
			{
				stringBuilder.Append(b.ToString("x2"));
			}

			_signatureParameterEntry.Value = stringBuilder.ToString();

			StringBuilder queryString = new StringBuilder();

			foreach (ParameterEntry pair in _parameters)
			{
				queryString.AppendQueryStringPair(pair.Key, pair.Value);
			}

			return queryString.ToString();
		}

		#endregion
	}
}
